﻿#if UNITY_EDITOR
#define NO_PROTECT
#endif

using Fish.Pattern.Observer;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;

namespace Fish.Utility
{
	public interface IProtectMonitor : IObservable<KeyUpdateData>
	{
		int Key { get; }
		int Count { get; }

		IEnumerator Start();
		void Stop();
	}

	public interface IProtect : IObserver<KeyUpdateData>
	{
		object Get(IProtectMonitor monitor);
	}

	public interface IProtect<T> : IProtect
	{
		T Get(IProtectMonitor monitor);
	}

	public struct KeyUpdateData
	{
		public int before;
		public int current;

		public KeyUpdateData(int before, int current)
		{
			this.before = before;
			this.current = current;
		}
	}

	public class ProtectMonitor : IProtectMonitor
	{
		private class Unsubscriber : IDisposable
		{
			private ICollection<IProtect> observerList;
			private IProtect observer;

			public Unsubscriber(ICollection<IProtect> observerList, IProtect observer)
			{
				this.observerList = observerList;
				this.observer = observer;
			}

			public void Dispose()
			{
				if (observer != null)
					observerList.Remove(observer);
			}
		}

		private Well512 generator;
		private ICollection<IProtect> dataSet = new HashSet<IProtect>();
		private bool isActive;

#if NO_PROTECT
		public static bool IsProtect { get; set; }
#endif

		public int Key { get; private set; }

		public int Count { get; private set; }

		private ProtectMonitor(int seed)
		{
			Init(seed);
		}

		private void Init(int seed)
		{
			generator = Well512.Create(seed);
			Key = 0;
			Count = 0;

			GenerateKey();
		}

		public static IProtectMonitor Create(int seed)
		{
			return new ProtectMonitor(seed);
		}

		private int GenerateKey()
		{
			Key = generator.NextInt();

			Count++;

			return Key;
		}

		public IEnumerator Start()
		{
			isActive = true;

			while (isActive)
			{
				KeyUpdateData keyData = new KeyUpdateData(Key, GenerateKey());

				foreach (var data in dataSet)
					data.OnNext(keyData);

				yield return new WaitForSeconds(0.25f);
			}
		}

		public void Stop()
		{
			isActive = false;
		}

		public IDisposable Subscribe(IObserver<KeyUpdateData> observer)
		{
			IProtect data = observer as IProtect;

			if (!dataSet.Contains(data))
				dataSet.Add(data);

			return new Unsubscriber(dataSet, data);
		}
	}

	public abstract class Protect<T> : IProtect<T>
	{
		#region Data
		private IDisposable unsubscriber;
		private HashAlgorithm hashAlgorithm;
		private byte[] hash;
		#endregion

		#region Init
		private Protect()
		{
			hashAlgorithm = MD5.Create();
		}

		protected Protect(T value, IProtectMonitor monitor)
			: this()
		{
			Subscribe(monitor);

			Set(value, monitor.Key);
		}
		#endregion

		#region Encrypt / Decrypt
		object IProtect.Get(IProtectMonitor monitor)
		{
			return Get(monitor);
		}

		public T Get(IProtectMonitor monitor)
		{
			var value = Decrypt(monitor.Key);

			Checksum(value);

			return value;
		}

		private void Set(T value, int key)
		{
			Crypto(value, out hash);

			Encrypt(value, key);
		}

		private T GetWithoutChecksum(int key)
		{
			return Decrypt(key);
		}

		protected abstract void Encrypt(T value, int key);

		protected abstract T Decrypt(int key);
		#endregion

		#region Checksum
		private void Crypto(T value, out byte[] result)
		{
			result = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(value.ToString()));
		}

		private void Checksum(T value)
		{
			if (hash == null)
				throw new Exception("not exist hash value which is needed to checksum");

			byte[] hashCurrent;

			Crypto(value, out hashCurrent);

			if (hashCurrent == null)
				throw new Exception("not exist hash value which is needed to checksum");

			bool isEqual = false;

			if (hashCurrent.Length == hash.Length)
			{
				int i = hashCurrent.Length;

				while (i < hashCurrent.Length && hashCurrent[i] == hash[i])
					i++;

				isEqual = i == hashCurrent.Length;
			}

			if (!isEqual)
				throw new Exception("both hash value are not equal");
		}
		#endregion

		#region Observer
		public virtual void OnCompleted() { }

		public virtual void OnError(Exception error) { }

		public virtual void OnNext(KeyUpdateData value)
		{
			Encrypt(GetWithoutChecksum(value.before), value.current);
		}

		public void Subscribe(IObservable<KeyUpdateData> provider)
		{
			unsubscriber = provider.Subscribe(this);
		}

		public void Unsubscribe()
		{
			if (unsubscriber != null)
				unsubscriber.Dispose();
		}
		#endregion
	}

	public class ProtectInt : Protect<int>
	{
		private int data;

		private ProtectInt(int value, IProtectMonitor monitor)
			: base(value, monitor) { }

		public static IProtect<int> Create(int value, IProtectMonitor monitor)
		{
#if NO_PROTECT
			if (ProtectMonitor.IsProtect)
#endif
			return new ProtectInt(value, monitor);
#if NO_PROTECT
			else
				return new NoProtect<int>(value);
#endif
		}

		protected override void Encrypt(int value, int key)
		{
			data = value ^ key;
		}

		protected override int Decrypt(int key)
		{
			return data ^ key;
		}
	}

	public class ProtectFloat : Protect<float>
	{
		private int data;

		private ProtectFloat(float value, IProtectMonitor monitor)
			: base(value, monitor) { }

		public static IProtect<float> Create(float value, IProtectMonitor monitor)
		{
#if NO_PROTECT
			if (ProtectMonitor.IsProtect)
#endif
			return new ProtectFloat(value, monitor);
#if NO_PROTECT
			else
				return new NoProtect<float>(value);
#endif
		}

		protected override void Encrypt(float value, int key)
		{
			data = (int)Math.Floor(value * 1000) ^ key;
		}

		protected override float Decrypt(int key)
		{
			return (data ^ key) * 0.001f;
		}
	}

#if NO_PROTECT
	public class NoProtect<T> : IProtect<T>
	{
		private T data;

		internal NoProtect(T value)
		{
			Set(value);
		}

		object IProtect.Get(IProtectMonitor monitor)
		{
			return Get(monitor);
		}

		public T Get(IProtectMonitor monitor)
		{
			return data;
		}

		private void Set(T value)
		{
			data = value;
		}

		public void OnCompleted() { }

		public void OnError(Exception error) { }

		public void OnNext(KeyUpdateData value) { }

		public void Subscribe(IObservable<KeyUpdateData> provider) { }

		public void Unsubscribe() { }
	}
#endif
}